﻿
/***************************************************************************

Copyright (c) Baseware.NET All rights reserved.
By Jesse Qu (jesse@baseware.net)

THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

***************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.Shell;
using System.Runtime.InteropServices;
using VSLangProj80;
using System.CodeDom.Compiler;
using System.Xml;
using System.IO;
using System.Data.Entity.Design;
using System.Data.Metadata.Edm;
using System.Globalization;
using System.Xml.Linq;
using System.CodeDom;

namespace Rongel.VisualStudio.Generator
{
	[ComVisible(true)] 
	[Guid("237B5CD4-FA6D-43d5-B93D-AEFBA268AA9A")]
	public class EntityCodeGenerator : BaseCodeGeneratorWithSite
	{
		protected override string GetDefaultExtension()
		{
			return (".Designer" + base.GetDefaultExtension());
		}

		protected override byte[] GenerateCode(string inputFileContent)
		{
            byte[] generatedCodeAsBytes = null;
            
            try
			{
				XElement csdlContent = ExtractCsdlContent(inputFileContent);
                if (csdlContent == null)
				{
					throw new InvalidOperationException("No CSDL content in input file");
				}

                foreach (EnvDTE.ProjectItem item in base.GetProjectItem().ProjectItems)
                    item.Remove();

                LanguageOption languageOption = GetLanguageOption();

                IEnumerable<XElement> csdlTypeContents = csdlContent.Elements();
                uint nElements = (uint)csdlTypeContents.Count<XElement>();
                uint i = 0;

                foreach (XElement csdlTypeContentElement in csdlTypeContents)
                {
                    if (csdlTypeContentElement.Name.LocalName != "EntityType")
                        continue;

                    XElement csdlTypeContent = CreateCsdlContent(csdlTypeContentElement, csdlContent);
                    XElement csdlRefContent = CreateRefCsdlContent(csdlTypeContentElement, csdlContent);

                    byte[] generatedCodeBytes = GenerateCodeInner(languageOption, csdlTypeContent, csdlRefContent, i, nElements);

                    XNamespace edmns = "http://schemas.microsoft.com/ado/2006/04/edm";
                    string fileName = csdlTypeContent.Element(edmns + "EntityType").Attribute("Name").Value + base.GetDefaultExtension();
                    fileName = Path.GetDirectoryName(base.InputFilePath) + "\\" + fileName;
                    if (File.Exists(fileName))
                        File.Delete(fileName);
                    File.WriteAllBytes(fileName, generatedCodeBytes);
                    base.GetProjectItem().ProjectItems.AddFromFile(fileName);
                                    
                    i++;
                }

                XElement csdlAssociationContent = CreateAssociationCsdlContent(csdlContent);
                XElement csdlAssociationRefContent = CreateAssociationRefCsdlContent(csdlContent);
                generatedCodeAsBytes = GenerateCodeInner(languageOption, csdlAssociationContent, csdlAssociationRefContent, 0, uint.MaxValue);
			}
			catch (Exception e)
			{
				base.GeneratorError(4, e.Message, 1, 1);
				generatedCodeAsBytes = null;
			}

			return generatedCodeAsBytes;
		}

        private LanguageOption GetLanguageOption()
        {
            LanguageOption languageOption = LanguageOption.GenerateCSharpCode;
            string fileExtension = base.GetCodeProvider().FileExtension;

            if (fileExtension != null && fileExtension.Length > 0)
                fileExtension = "." + fileExtension.TrimStart(".".ToCharArray());

            if (fileExtension.EndsWith(".vb", StringComparison.InvariantCultureIgnoreCase))
                languageOption = LanguageOption.GenerateVBCode;
            else if (fileExtension.EndsWith(".cs", StringComparison.InvariantCultureIgnoreCase))
                languageOption = LanguageOption.GenerateCSharpCode;
            else
                throw new InvalidOperationException("Unsupported project language. Only C# and VB are supported.");
            return languageOption;
        }

        private byte[] GenerateCodeInner(LanguageOption languageOption, XElement csdlTypeContent, XElement csdlRefContent, uint i, uint nElements)
        {
            byte[] generatedCodeAsBytes = null;
            using (XmlReader csdlReader = csdlTypeContent.CreateReader())
            {
                using (XmlReader csdlRefReader = csdlRefContent.CreateReader())
                {
                    using (StringWriter codeWriter = new StringWriter(CultureInfo.InvariantCulture))
                    {
                        if (base.CodeGeneratorProgress != null && nElements != uint.MaxValue)
                            base.CodeGeneratorProgress.Progress((i * 100) + 33, 100 * nElements);

                        EntityClassGenerator classGenerator = new EntityClassGenerator(languageOption);
                        classGenerator.OnTypeGenerated += new TypeGeneratedEventHandler(OnTypeGenerated);
                        classGenerator.OnPropertyGenerated += new PropertyGeneratedEventHandler(OnPropertyGenerated);

                        if (base.CodeGeneratorProgress != null && nElements != uint.MaxValue)
                            base.CodeGeneratorProgress.Progress((i * 100) + 66, 100 * nElements);

                        List<XmlReader> refXmlReaderList = new List<XmlReader>();
                        refXmlReaderList.Add(csdlRefReader);

                        classGenerator.EdmToObjectNamespaceMap.Add(csdlTypeContent.Attribute("Namespace").Value, base.FileNameSpace);

                        IList<EdmSchemaError> errors = classGenerator.GenerateCode(csdlReader, codeWriter, refXmlReaderList);

                        if (errors != null)
                        {
                            foreach (EdmSchemaError error in errors)
                            {
                                int line = (error.Line == 0) ? 0 : (error.Line - 1);
                                int column = (error.Column == 0) ? 0 : (error.Column - 1);
                                string entityName = csdlTypeContent.Name.LocalName + csdlTypeContent.Attribute("Name").Value + ": ";

                                if (error.Severity == EdmSchemaErrorSeverity.Warning)
                                    base.GeneratorWarning(0, entityName + error.Message, (uint)line, (uint)column);
                                else
                                    base.GeneratorError(4, entityName + error.Message, (uint)line, (uint)column);
                            }
                        }

                        generatedCodeAsBytes = Encoding.UTF8.GetBytes(codeWriter.ToString());

                        if (base.CodeGeneratorProgress != null && nElements != uint.MaxValue)
                            base.CodeGeneratorProgress.Progress((i * 100) + 100, 100 * nElements);
                    }
                }
            }
            return generatedCodeAsBytes;
        }

		private void OnTypeGenerated(object sender, TypeGeneratedEventArgs eventArgs)
		{
			eventArgs.AdditionalAttributes.AddRange(CreateCodeAttributes(eventArgs.TypeSource));
		}

		private void OnPropertyGenerated(object sender, PropertyGeneratedEventArgs eventArgs)
		{
			eventArgs.AdditionalAttributes.AddRange(CreateCodeAttributes(eventArgs.PropertySource));
		}

		private IList<CodeAttributeDeclaration> CreateCodeAttributes(MetadataItem item)
		{
			string xmlns = "http://tempuri.org/SampleAnnotations";

			List<CodeAttributeDeclaration> codeAttributeDeclarations = new List<CodeAttributeDeclaration>();
			if (item != null)
			{
				IEnumerable<MetadataProperty> metadataProperties = item.MetadataProperties.Where(prop => prop.Name.StartsWith(xmlns));
				foreach (MetadataProperty metadataProperty in metadataProperties)
				{
					string metadataPropertyValue = (string)metadataProperty.Value;
					if (!String.IsNullOrEmpty(metadataPropertyValue))
					{
						string[] attributes = metadataPropertyValue.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
						foreach (string attribute in attributes)
						{
							string attributeName = attribute;
							string[] attributeParams = new string[1];

							if(attribute.Contains('('))
							{
								attributeParams = attribute.Split(new char[] {'(', ')'}, StringSplitOptions.RemoveEmptyEntries);
								attributeName = attributeParams[0];
							}

							CodeAttributeDeclaration codeAttributeDeclaration = new CodeAttributeDeclaration(attributeName);

							foreach (string attributeParam in attributeParams.Skip(1))
							{
								object attributeParamObj = null;
								bool attributeParamBool = false;
								if (bool.TryParse(attributeParam, out attributeParamBool))
								{
									attributeParamObj = attributeParamBool;
								}
								else
								{
									attributeParamObj = attributeParam;
								}
								codeAttributeDeclaration.Arguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression(attributeParamObj)));
							}
							codeAttributeDeclarations.Add(codeAttributeDeclaration);
						}
					}
				}
			}
			return codeAttributeDeclarations;
		}

		private XElement ExtractCsdlContent(string inputFileContent)
		{
			XElement csdlContent = null;
			XNamespace edmxns = "http://schemas.microsoft.com/ado/2007/06/edmx";
			XNamespace edmns = "http://schemas.microsoft.com/ado/2006/04/edm";

			XDocument edmxDoc = XDocument.Load(new StringReader(inputFileContent));
			if (edmxDoc != null)
			{
				XElement edmxNode = edmxDoc.Element(edmxns + "Edmx");
				if (edmxNode != null)
				{
					XElement runtimeNode = edmxNode.Element(edmxns + "Runtime");
					if (runtimeNode != null)
					{
						XElement conceptualModelsNode = runtimeNode.Element(edmxns + "ConceptualModels");
						if (conceptualModelsNode != null)
						{
							csdlContent = conceptualModelsNode.Element(edmns + "Schema");
						}
					}
				}
			}
			return csdlContent;
		}

        private XElement CreateCsdlContent(XElement csdlTypeContent, XElement csdlRefContent)
        {
            XElement csdlContent = new XElement(csdlRefContent);
            csdlContent.RemoveNodes();
            csdlContent.Add(new XElement(csdlTypeContent));
            return csdlContent;
        }

        private XElement CreateRefCsdlContent(XElement csdlTypeContent, XElement csdlRefContent)
        {
            XElement csdlContent = new XElement(csdlRefContent);
            foreach (XElement element in csdlContent.Elements())
            {
                if (element.NodeType == csdlTypeContent.NodeType && element.Name == csdlTypeContent.Name)
                {
                    string elementName = element.Attribute("Name").Value.ToLower();
                    string typeName = csdlTypeContent.Attribute("Name").Value.ToLower();

                    if (elementName == typeName)
                    {
                        element.Remove();
                        break;
                    }
                }
            }
            return csdlContent;
        }

        private XElement CreateAssociationCsdlContent(XElement csdlContent)
        {
            XElement csdlAssociationContent = new XElement(csdlContent);
            csdlAssociationContent.RemoveNodes();
            IEnumerable<XElement> csdlTypeContentElements = csdlContent.Elements();
            foreach (XElement csdlTypeContentElement in csdlTypeContentElements)
            {
                if (csdlTypeContentElement.Name.LocalName != "Association")
                    continue;
                csdlAssociationContent.Add(new XElement(csdlTypeContentElement));
            }
            return csdlAssociationContent;
        }

        private XElement CreateAssociationRefCsdlContent(XElement csdlContent)
        {
            XElement csdlAssociationRefContent = new XElement(csdlContent);
            csdlAssociationRefContent.RemoveNodes();
            IEnumerable<XElement> csdlTypeContentElements = csdlContent.Elements();
            foreach (XElement csdlTypeContentElement in csdlTypeContentElements)
            {
                if (csdlTypeContentElement.Name.LocalName == "Association")
                    continue;
                csdlAssociationRefContent.Add(new XElement(csdlTypeContentElement));
            }
            return csdlAssociationRefContent;
        }
 	}
}
